Android 进行 HTTPS 网络通信

遇到的问题

前两天在 Andorid 上与使用自签名证书的服务器进行 https 网络通信遇到了问题,主要的问题出在于服务器端的证书不受客户端信任与认证,服务器端也不认识客户端,双方互不认识(在浏览器好歹也会提示用户添加安全证书)。


问题分析

根据需求分析,Android 平台上需要进行双向验证才能够进行正常的 https 通信。而在之前的代码结构中,由于不熟悉 https 沟通方式,错误使用了服务端证书来进行身份识别与验证。


解决方式

进行后续操作的调整,在 centOS 平台上使用 keytool 分别了生成了服务器端证书以及客户端证书,并将客户端证书放置 Android 上,用于连接服务器端时对服务器端的证书进行鉴别与认证。

具体操作如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
1、生成服务器证书库

keytool -validity 365 -genkey -v -alias server -keyalg RSA -keystore server.keystore -dname "CN=commonName,OU=organizationalUnit,O=Organization,L=Locality,ST=state,c=country" -storepass 123456 -keypass 123456 -keysize 2048

2、生成客户端证书库

keytool -validity 365 -genkey -v -alias client -keyalg RSA -storetype PKCS12 -keystore client.p12 -dname "CN=client,OU=organizationalUnit,O=Organization,L=Organization,ST=state,c=country" -storepass 123456 -keypass 123456 -keysize 2048

3、从客户端证书库中导出客户端证书

keytool -export -v -alias client -keystore client.p12 -storetype PKCS12 -storepass 123456 -rfc -file client.cer

4、从服务器证书库中导出服务器证书

keytool -export -v -alias server -keystore server.keystore -storepass 123456 -rfc -file server.cer

5、生成客户端信任证书库(由服务端证书生成的证书库)

keytool -import -v -alias server -file server.cer -keystore client.truststore -storepass 123456 -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider

6、将客户端证书导入到服务器证书库(使得服务器信任客户端证书)

keytool -import -v -alias client -file client.cer -keystore server.keystore -storepass 123456

keytool 常用参数说明:

参数 用途
-genkey 在用户主目录中创建一个默认文件“.keystore”
-validity 指定创建的证书有效期为多少天
-alias 产生别名,每个 keystore 都关联一个独一无二的 alias
-keystore 指定密钥库的名称
-keyalg 指定密钥的算法
-keysize 指定密钥的长度
-dname 指定证书发行者信息,其中: “CN=名字与姓氏,OU=组织单位名称,O=组织名称,L=城市或区域名 称,ST=州或省份名称,C=单位的两字母国家代码”
-storepass 指定密钥库的密码(.keystore密码)
-keypass 指定别名条目的密码(私钥密码)
-storetype 指定密钥库的存储类型
-export 将 alias 指定的证书导出到文件
-import 将已签名的证书导入密钥库中
-rfc 以Base64的编码格式打印证书
-file 指定导出到文件的文件名
-v 查看密钥库中的证书详细信息

在代码结构上,在 http 通信代码中添加 SSL 通信机制,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public static final String KEY_STORE_TYPE_BKS = "BKS";//证书类型
public static final String KEY_STORE_TYPE_P12 = "PKCS12";//证书类型
public static final String KEY_STORE_CLIENT_PATH = "swaypay.p12";//客户端要给服务器端认证的证书
public static final String KEY_STORE_TRUST_PATH = "swaypay.truststore";//客户端验证服务器端的证书库
public static final String KEY_STORE_PASSWORD = "superssl1";// 客户端证书密码
public static final String KEY_STORE_TRUST_PASSWORD = "superssl1";//客户端证书库密码
public static final String KEY_STORE_TYPE_X509 = "X509";
public static final String SSL_CONTEXT_PROTOCOL = "TLS";

public void setSSLConnection() {
// 1 - KeyStore - 用于存储各种类型的密钥,方便管理与使用
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE_P12);
KeyStore trustStore = KeyStore.getInstance(KEY_STORE_TYPE_BKS);

// 2 - 将提前获取的受服务器端信任的客户端证书/用于验证服务器端的证书的证书库进行导入
InputStream ksIn = ContextHolder.getContext().getAssets().open(KEY_STORE_CLIENT_PATH);
InputStream tsIn = ContextHolder.getContext().getAssets().open(KEY_STORE_TRUST_PATH);

// 3 - 初始化 KeyManagerFactory
keyStore.load(ksIn, KEY_STORE_PASSWORD.toCharArray());
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KEY_STORE_TYPE_X509);
keyManagerFactory.init(keyStore, KEY_STORE_PASSWORD.toCharArray());

// 4 - 初始化 TrustManagerFactory
trustStore.load(tsIn, KEY_STORE_TRUST_PASSWORD.toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);

// 5 - 初始化 SSLContext 并添加通过 KeyManagerFactory 和 TrustManagerFactory 分别生成的 getKeyManagers 和 getTrustManagers
// 这一步中,通过这种方式,才算 https 的双向验证机制的真正建立
sslContext = SSLContext.getInstance(SSL_CONTEXT_PROTOCOL);
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
}

...

private HttpURLConnection createHttpURLConnection(String targetUrl) {
...
// 6
if (httpURLConnection instanceof HttpsURLConnection) {
setSSLConnection();
((HttpsURLConnection) httpURLConnection).setSSLSocketFactory(sslContext.getSocketFactory());
// 暂不验证 host 有效性
((HttpsURLConnection) httpURLConnection).setHostnameVerifier((String hostname, SSLSession session) -> {
return true;
});
}
...
}


参考资料:

[1] http://frank-zhu.github.io/android/2014/12/26/android-https-ssl/